#![deny(missing_docs)]

//! A crate wrapping [tcc](https://bellard.org/tcc/) in Rust.

mod bindings;

use crate::bindings::*;
use std::ffi::{CStr, CString};
use std::fmt;
use std::mem;
use std::os::raw::c_void;
use std::path::Path;
use std::ptr::null_mut;
use std::rc::Rc;

#[derive(Debug, Clone, Copy)]
#[repr(u32)]
/// Output type of the compilation.
pub enum OutputType {
    /// output will be run in memory (default)
    Memory = TCC_OUTPUT_MEMORY,

    /// executable file
    Exe = TCC_OUTPUT_EXE,

    /// dynamic library
    Dll = TCC_OUTPUT_DLL,

    /// object file
    Obj = TCC_OUTPUT_OBJ,

    /// only preprocess (used internally)
    Preprocess = TCC_OUTPUT_PREPROCESS,
}

extern "C" fn callback(user_data: *mut c_void, string: *const i8) {
    let tcc = unsafe { &mut *(user_data as *mut Tcc) };
    if let Some(ref func) = tcc.error_func {
        let string = unsafe { CStr::from_ptr(string) }.to_str().unwrap();
        func(string);
    }
}

/// Main struct of this crate, can be used to compile C code into machine code.
pub struct Tcc {
    inner: *mut TCCState,
    error_func: Option<Box<dyn Fn(&str)>>,
}

impl fmt::Debug for Tcc {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "Tcc")
    }
}

impl Default for Tcc {
    fn default() -> Tcc {
        Tcc::new()
    }
}

impl Tcc {
    /// create a new TCC compilation context
    pub fn new() -> Self {
        Tcc {
            inner: unsafe { tcc_new() },
            error_func: None,
        }
    }

    /// set CONFIG_TCCDIR at runtime
    pub fn set_lib_path<P: AsRef<Path>>(&mut self, path: P) {
        let path = path.as_ref().to_str().unwrap();
        unsafe { tcc_set_lib_path(self.inner, path.as_ptr() as *const i8) }
    }

    /// set error/warning display callback
    pub fn set_error_func(&mut self, error_func: Option<Box<dyn Fn(&str)>>) {
        unsafe {
            tcc_set_error_func(
                self.inner,
                self as *mut Tcc as *mut c_void,
                if error_func.is_some() {
                    Some(callback)
                } else {
                    None
                },
            )
        };
        self.error_func = error_func;
    }

    /// set options as from command line (multiple supported)
    pub fn set_options(&mut self, options: &str) {
        let options = CString::new(options).unwrap();
        unsafe { tcc_set_options(self.inner, options.as_ptr()) };
    }

    /// add include path
    pub fn add_include_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ()> {
        let path = path.as_ref().to_str().unwrap();
        let ret = unsafe { tcc_add_include_path(self.inner, path.as_ptr() as *const i8) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// add in system include path
    pub fn add_sysinclude_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ()> {
        let path = path.as_ref().to_str().unwrap();
        let ret = unsafe { tcc_add_sysinclude_path(self.inner, path.as_ptr() as *const i8) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// define preprocessor symbol 'sym'. Can put optional value
    pub fn define_symbol(&mut self, sym: &str, value: &str) {
        let sym = CString::new(sym).unwrap();
        let value = CString::new(value).unwrap();
        unsafe { tcc_define_symbol(self.inner, sym.as_ptr(), value.as_ptr()) }
    }

    /// undefine preprocess symbol 'sym'
    pub fn undefine_symbol(&mut self, sym: &str) {
        let sym = CString::new(sym).unwrap();
        unsafe { tcc_undefine_symbol(self.inner, sym.as_ptr()) }
    }

    /// add a file (C file, dll, object, library, ld script)
    pub fn add_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ()> {
        let path = path.as_ref().to_str().unwrap();
        let ret = unsafe { tcc_add_file(self.inner, path.as_ptr() as *const i8) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// compile a string containing a C source
    pub fn compile_string(&mut self, buf: &str) -> Result<(), ()> {
        let buf = CString::new(buf).unwrap();
        let ret = unsafe { tcc_compile_string(self.inner, buf.as_ptr()) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// set output type. MUST BE CALLED before any compilation
    pub fn set_output_type(&mut self, output_type: OutputType) -> Result<(), ()> {
        let ret = unsafe { tcc_set_output_type(self.inner, output_type as i32) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// equivalent to -Lpath option
    pub fn add_library_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ()> {
        let path = path.as_ref().to_str().unwrap();
        let ret = unsafe { tcc_add_library_path(self.inner, path.as_ptr() as *const i8) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// the library name is the same as the argument of the '-l' option
    pub fn add_library(&mut self, name: &str) -> Result<(), ()> {
        let name = CString::new(name).unwrap();
        let ret = unsafe { tcc_add_library(self.inner, name.as_ptr()) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// add a symbol to the compiled program
    pub fn add_symbol<S: Symbol>(&mut self, name: &str, symbol: *const c_void) -> Result<(), ()> {
        let name = CString::new(name).unwrap();
        let ret = unsafe { tcc_add_symbol(self.inner, name.as_ptr(), symbol) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// output an executable, library or object file
    pub fn output_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ()> {
        let path = path.as_ref().to_str().unwrap();
        let ret = unsafe { tcc_output_file(self.inner, path.as_ptr() as *const i8) };
        if ret == -1 {
            return Err(());
        }
        Ok(())
    }

    /// link and run main() function and return its value
    pub fn run(&mut self, args: &[&str]) -> Result<(), ()> {
        let argc = args.len();
        let mut argv: Vec<*mut i8> = args.iter()
            .map(|s| CString::new(*s).unwrap().into_raw()).collect();
        let ret = unsafe { tcc_run(self.inner, argc as i32, argv.as_mut_ptr()) };
        if ret == -1 { return Err(()); }
        let _: Vec<CString> = argv.into_iter()
            .map(|ptr| unsafe { CString::from_raw(ptr) }).collect();
        Ok(())
    }

    /// do all relocations
    pub fn relocate(self) -> Result<RelocatedTcc, Tcc> {
        let len = unsafe { tcc_relocate(self.inner, null_mut()) };
        if len == -1 {
            return Err(self);
        }
        let mut memory = Vec::with_capacity(len as usize);
        let ret = unsafe { tcc_relocate(self.inner, memory.as_mut_ptr() as *mut c_void) };
        if ret == -1 {
            return Err(self);
        }
        unsafe { memory.set_len(len as usize) }
        let relocated = RelocatedTcc {
            inner: self.inner,
            memory: Rc::new(memory),
        };
        // We need this in order to not deallocate self.inner in the Drop impl.
        mem::forget(self);
        Ok(relocated)
    }
}

impl Drop for Tcc {
    fn drop(&mut self) {
        unsafe { tcc_delete(self.inner) }
    }
}

/// Once `Tcc::relocate()` is done successfully, this struct is returned to let you fetch the
/// symbols from the generated machine code.
pub struct RelocatedTcc {
    inner: *mut TCCState,
    memory: Rc<Vec<u8>>,
}

impl RelocatedTcc {
    /// return symbol value or NULL if not found
    pub fn get_symbol(&mut self, name: &str) -> Option<*const c_void> {
        let name = CString::new(name).unwrap();
        unsafe {
            let ptr = tcc_get_symbol(self.inner, name.as_ptr());
            if ptr.is_null() { None } else { Some(ptr) }
        }
    }

    /// list all symbols
    pub fn list_symbols(&mut self, callback: impl FnMut(&str, usize)) {
        use std::os::raw::*;
        use std::mem::transmute;

        extern "C" fn c_callback(ctx: *mut c_void, name: *const c_char, address: *const c_void) {
            unsafe {
                let callback = transmute::<_, &mut Box<dyn FnMut(&str, usize)>>(ctx);
                callback(CStr::from_ptr(name).to_str().unwrap_or(""), address as usize);
            }
        }
        unsafe {
            let mut callback: Box<dyn FnMut(&str, usize)> = Box::new(callback);
            tcc_list_symbols(self.inner, transmute(&mut callback), c_callback);
        }
    }
}

impl fmt::Debug for RelocatedTcc {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "RelocatedTcc")
    }
}

impl Drop for RelocatedTcc {
    fn drop(&mut self) {
        unsafe { tcc_delete(self.inner) }
        // self.memory will only be dropped once the last CSymbol is dropped.
    }
}

/// Symbol trait, implemented by either a `CSymbol` returned by `TccRelocated::get_symbol()`, or
/// by any `*const c_void` (for instance an `extern "C"` function).
pub trait Symbol {
    /// How to obtain a pointer to pass to `Tcc::add_symbol()`.
    fn as_ptr(&self) -> *const c_void;
}

/// A C symbol returned by `TccRelocated::get_symbol()`.
pub struct CSymbol {
    ptr: *const c_void,

    name: String,

    #[allow(dead_code)]
    memory: Rc<Vec<u8>>,
}

impl fmt::Debug for CSymbol {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "CSymbol({:?} at {:p})", self.name, self.ptr)
    }
}

impl Symbol for CSymbol {
    fn as_ptr(&self) -> *const c_void {
        self.ptr
    }
}

impl Symbol for *const c_void {
    fn as_ptr(&self) -> *const c_void {
        *self
    }
}
